/**
 * Copyright (C) 2011 GreenSoft S.A.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *        http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ar.com.greensoft.neosystem.dao.jpa;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceException;
import javax.persistence.TransactionRequiredException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;

import org.jboss.logging.Logger;

import ar.com.greensoft.neosystem.dao.DaoException;
import ar.com.greensoft.neosystem.dao.GenericDao;
import ar.com.greensoft.neosystem.dao.searching.SortCriteria;
import ar.com.greensoft.neosystem.search.SearchFilter;

/**
 * 
 * @author Jorge E. Villaverde
 * @since 07/07/2013
 * @version 1.0.0
 */
public abstract class CommonJpaDaoImpl<T, PK extends Serializable>
	implements GenericDao<T, PK>, Serializable{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	protected static final Logger logger = Logger.getLogger(AbstractJpaDaoImpl.class);

	// Constants ----------------------------------------------------------------------------------
	
	// Properties ---------------------------------------------------------------------------------

	/**
	 * Expression Map for sorting
	 */
	protected Map<String, Expression<?>> expressionMap;
	
	// Dependencies -------------------------------------------------------------------------------
	
	@PersistenceContext
	protected EntityManager entityManager;

	// Methods ------------------------------------------------------------------------------------

	protected Map<String, Expression<?>> getExpressionMap(Root<T> root) {
		if(expressionMap == null){
			expressionMap = new HashMap<String, Expression<?>>();
		}
		return expressionMap;
	}	

	protected List<Order> createOrders(Root<T> root){
		return null;
	}
	
	@Override
	public long count() {
		if(logger.isTraceEnabled())
			logger.trace("Counting Entities in the Repository");
				
		Class<T> entityClazz = getEntityClazz();		
		
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		
		CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
		
		criteriaQuery.select(
				criteriaBuilder.count(
						criteriaQuery.from(entityClazz)));
		
		return (Long)entityManager.createQuery(criteriaQuery).getSingleResult();
	}
	
	@Override
	public long countBySearchFilter(SearchFilter searchFilter) {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<Long> countCriteria = criteriaBuilder.createQuery(Long.class);
		Root<T> root = countCriteria.from(getEntityClazz());
		
		countCriteria.select(criteriaBuilder.count(root));
		
		Expression<Boolean> restriction  = createWhereFromSearchFilter(searchFilter, root);
		if(restriction != null){
			countCriteria.where(restriction);
		}
		
		TypedQuery<Long> typedQuery = entityManager.createQuery(countCriteria);
		return typedQuery.getSingleResult();
	}	

	@Override
	public List<T> findAll() throws DaoException {
		if(logger.isTraceEnabled())
			logger.trace("Returning the list of " + getEntityClazz().getSimpleName());
		
		Class<T> entityClazz = getEntityClazz();		
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClazz);
		criteriaQuery.from(getEntityClazz());
		
		TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
		return typedQuery.getResultList();
	}
	
	@Override
	public T merge(T persistentObject) throws DaoException {
		if(logger.isTraceEnabled())
			logger.trace("Merging Entity: " + persistentObject);
		try {
			T mergeEntity = entityManager.merge(persistentObject);
			entityManager.flush();
			return mergeEntity;
		} catch (PersistenceException e){
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.error(e.getMessage());
		}
		return null;
	}
	
	@Override
	public void persist(T transientObject) throws DaoException {
		if(logger.isTraceEnabled())
			logger.trace("Persinting Entity: " + transientObject);
		try {
			entityManager.persist(transientObject);
			entityManager.flush();
		} catch (PersistenceException e){
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.error(e.getMessage());
			throw new DaoException(e.getCause());
		}
	}

	@Override
	public void refresh(T persistentObject) throws DaoException {
		if(logger.isTraceEnabled())
			logger.trace("Refressing Entity: " + persistentObject);
		try {
			entityManager.refresh(persistentObject);
		} catch (IllegalArgumentException e) {
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (TransactionRequiredException e) {
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (EntityNotFoundException e) { 
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		}
	}

	@Override
	public void remove(T persistentObject) throws DaoException {
		try {
			T entity = entityManager.merge(persistentObject);
			entityManager.remove(entity);
			entityManager.flush();
		} catch (IllegalArgumentException e) {
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (TransactionRequiredException e) {
			logger.warn(e.getMessage());
			throw new DaoException(e.getCause());
		} catch (Exception e) {
			logger.error(e.getMessage());
		}
	}
		
	@Override
	public List<T> findEntries(int startPosition, int maxResult) throws DaoException {
		return findEntries(startPosition, maxResult, null);
	}
	
	@Override
	public List<T> findEntries(int startPosition, int maxResult, List<SortCriteria> sortCriterias) throws DaoException {
		if(logger.isTraceEnabled())
			logger.trace("Returning the list of Entities from: " + startPosition + " to " + (startPosition+maxResult));
					
		Class<T> entityClazz = getEntityClazz();		
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClazz);
		Root<T> root = criteriaQuery.from(entityClazz);
		
		List<Order> orders = createEntityOrders(criteriaBuilder, 
				getExpressionMap(root), 
				sortCriterias);
		
		if(orders == null)
			orders = createOrders(root);
		if(orders != null)
			criteriaQuery.orderBy(orders);
		
		TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
		typedQuery.setFirstResult(startPosition);
		typedQuery.setMaxResults(maxResult);
		
		return typedQuery.getResultList();
	}
	
	@Override
	public List<T> findEntriesBySearchFilter(SearchFilter searchFilter, int startPosition, int maxResult) {
		return findEntriesBySearchFilter(searchFilter, startPosition, maxResult, null);
	}
	
	@Override
	public List<T> findEntriesBySearchFilter(SearchFilter searchFilter, 
			int startPosition, 
			int maxResult,
			List<SortCriteria> sortCriterias) {
		if(logger.isTraceEnabled())
			logger.trace("Returning the list of Entities from: " + startPosition + " to " + (startPosition+maxResult) + " by SeachInput");

		Class<T> entityClazz = getEntityClazz();		
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClazz);
		Root<T> root = criteriaQuery.from(entityClazz);
		
		criteriaQuery.select(root);
		Expression<Boolean> predicate = createWhereFromSearchFilter(searchFilter, root);
		if(predicate != null)
			criteriaQuery.where(predicate);
		
		List<Order> orders = createEntityOrders(criteriaBuilder, 
				getExpressionMap(root), 
				sortCriterias);

		if(orders != null && !orders.isEmpty()){
			criteriaQuery.orderBy(orders);			
		}
		
		TypedQuery<T> typedQuery = entityManager.createQuery(criteriaQuery);
		typedQuery.setFirstResult(startPosition);
		typedQuery.setMaxResults(maxResult);
		
		return typedQuery.getResultList();
	}
	
	// Static Methods -----------------------------------------------------------------------------

	/**
	 * Generar una lista de ordenes a partir de lista de SortOrdes 
	 * @param builder
	 * @param expressionMap
	 * @param sortCriterias
	 * @return
	 */
	protected static List<Order> createEntityOrders(CriteriaBuilder builder, Map<String, Expression<?>> expressionMap, List<SortCriteria> sortCriterias) {
		if(sortCriterias == null || sortCriterias.isEmpty())
			return null;
		
		List<Order> ret = new ArrayList<Order>(sortCriterias.size());
		
		for(int i=0; i < sortCriterias.size();i++){
			SortCriteria sortCriteria = sortCriterias.get(i);
			
			Expression<?> expression = expressionMap.get(sortCriteria.getAttribute());
			if(expression == null)
				continue;
				
			Order order = null;
			if(sortCriteria.isAscending()){
				order = builder.asc(expression);
			}else{
				order = builder.desc(expression);				
			}
			ret.add(order);
		}
		return ret;
	}
	
	// Abstract Methods ---------------------------------------------------------------------------

	/**
	 * Class of the entity
	 * @return 
	 */
	protected abstract Class<T> getEntityClazz();

	protected abstract Expression<Boolean> createWhereFromSearchFilter(SearchFilter searchFilter, Root<T> root);

}
